<!-- TinyAiEditor.vue -->
<template>
  <!-- 保留原容器 ID，避免样式和 DOM 查询失效 -->
  <div class="editor-shell" id="sample">
    <Editor
      id="uuid"
      licenseKey="gpl"
      :tinymceScriptSrc="tinymceScriptSrc"
      :init="initOptions"
      v-model="content"
      :onInit="handleInit"
    />
  </div>
</template>

<script lang="ts" setup>
import Editor from '@tinymce/tinymce-vue'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { ref, watch } from 'vue'
import { Session } from '/@/utils/storage'
import other from '/@/utils/other'

/** 可配但默认保持原来的路径与语言 */
const props = defineProps({
  html: { type: String, default: '' },
  baseUrl: { type: String, default: import.meta.env.VITE_API_URL },
  tinymceScriptSrc: { type: String, default: '/tinymce/tinymce.min.js' },
  skinUrl: { type: String, default: '/tinymce/skins/ui/oxide' },
  language: { type: String, default: 'zh_CN' },
  enableAI: { type: Boolean, default: true }, // 允许关闭内置 AI（可选）
	modelName: { type: String, default: '' }, // 润色等功能使用的模型名称
})

const emit = defineEmits(['update:html','change','init'])

/** 与编辑器绑定的内部值 */
const content = ref(props.html || '')
const editorRef = ref<any>(null)
const isStreaming = ref(false) // 防止父级 props 回填打架

/** 等待就绪 Promise：父组件或内部方法都能 await */
let _resolveReady: (ed:any)=>void
const readyPromise = new Promise<any>(r => { _resolveReady = r })

/** 父组件 -> 子组件 同步 */
watch(() => props.html, (v) => {
  if (isStreaming.value) return // 流式期间忽略父级覆盖
  if (v === content.value) return
  content.value = v ?? ''
  if (editorRef.value && editorRef.value.getContent() !== content.value) {
    editorRef.value.setContent(content.value)
  }
})

/** onInit：拿到 editor 实例 */
const handleInit = (_evt:any, editor:any) => {
  editorRef.value = editor
  if (props.html && editor.getContent() !== props.html) {
    editor.setContent(props.html)
  }
  emit('init', editor)
  _resolveReady?.(editor)  // 标记已就绪
}

/** 子组件 -> 父组件 同步：当 content 改变（来自编辑器） */
watch(content, (v) => {
  emit('update:html', v)
  emit('change', v)
})

/** ===== 对外暴露 ===== */
function waitForReady(){ return readyPromise }
function getHtml(){ return content.value }
function getEditor(){ return editorRef.value }
function setHtml(v:string){
  content.value = v ?? ''
  if (editorRef.value && editorRef.value.getContent() !== content.value) {
    editorRef.value.setContent(content.value)
  }
}

/* ======= TinyMCE 配置（保持你的原样） ======= */
const toolbar = [
  { name: 'history', items: ['undo', 'redo'] },
  { name: 'formatting', items: ['bold', 'italic', 'underline', 'strikethrough', 'lineheight'] },
  { name: 'styles', items: ['fontfamily', 'fontsize', 'styles'] },
  { name: 'alignment', items: ['alignleft', 'aligncenter', 'alignright', 'alignjustify'] },
  { name: 'color', items: ['forecolor', 'backcolor'] },
  { name: 'insert', items: ['table'] },
  { name: 'list', items: ['numlist', 'bullist'] },
  { name: 'indentation', items: ['outdent', 'indent'] },
  { name: 'split', items: ['hr', 'blockquote'] },
  { name: 'icon', items: ['emoticons', 'charmap'] },
  // { name: 'file', items: ['image', 'media', 'link'] },
  { name: 'code', items: ['code', 'codesample'] },
  { name: 'other', items: ['searchreplace', 'print', 'aiDocGen'] }
]

const content_style = ref(`
@page { size: A4; margin: 2cm; }
html { background: transparent; }
@media screen {
  body.doc-page {
    width: min(1100px, 92vw);
    min-height: 100vh;
    margin: 0 auto;
    padding: 5% 6%;
    box-sizing: border-box;
    background: #fff;
    box-shadow: 0 10px 30px rgba(0,0,0,.20), 0 0 0 1px rgba(0,0,0,.06);
    border-radius: 0;
    color: #000;
    font-family: 仿宋,FangSong,STFangsong,serif;
    font-size: 12pt;
    line-height: 1;
  }
}
@media print {
  body.doc-page {
    width: auto; height: auto; margin: 0; padding: 0;
    box-shadow: none; border-radius: 0; background: #fff;
  }
  img { max-width: 100% !important; height: auto !重要; }
}
h1,h2,h3 { margin: 1.2em 0 .6em; }
p { margin: .8em 0; }
table { border-collapse: collapse; width: 100%; }
table, th, td { border: 1px solid #e5e7eb; }
th, td { padding: .5em .75em; }
html { scrollbar-width: none; }
html::-webkit-scrollbar { display: none; }
`)
const font_family_formats = ref("宋体=宋体,宋体-简,宋体-繁,华文宋体,simsun,SimSun,STSong,serif;仿宋体=仿宋,FangSong,STFangsong,serif;黑体=SimHei,sans-serif;楷体=楷体,楷体-简,楷体-繁,KaiTi,STKaiti,华文楷体,serif;小标宋=小标宋,FZXBSJW-GB1-0,小标宋,sans-serif;仿宋_GB2312=仿宋_GB2312;楷体_GB2312=楷体_GB2312;Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats")
const font_size_formats = ref("初号=44pt 小初=36pt 一号=26pt 小一=24pt 二号=22pt 小二=18pt 三号=16pt 小三=15pt 四号=14pt 小四=12pt 五号=10.5pt 小五=9pt 六号=7.5pt 小六=6.5pt 七号=5.5pt 八号=5pt")
const line_height_formats = ref("1 1.1 1.2 1.3 1.4 1.5 1.75 2 2.5 28pt")
const plugins = ref([
  'advlist','anchor','autolink','autosave','code','codesample','directionality',
  'fullscreen','help','image','insertdatetime','link','lists','media','nonbreaking',
  'pagebreak','preview','save','searchreplace','table',
  'visualblocks','wordcount','quickbars',
])

/* ========= AI 快捷按钮（稳健：非可编辑占位符 + replaceWith + 书签兜底） ========= */
const setupFunction = (editor:any) => {
  const runAI = async (action:string) => {
    const selText = editor.selection.getContent({ format: 'text' }).trim();
    if (!selText) {
      editor.notificationManager.open({ text: '请选择要处理的文本', type: 'warning' });
      return;
    }

    const bookmark = editor.selection.getBookmark(2, true); // 回滚书签
    const doc = editor.getDoc();
    const ph = doc.createElement('span');
    ph.id = 'ai-ph-' + Date.now();
    ph.setAttribute('data-ai-ph', ph.id);
    ph.setAttribute('contenteditable', 'false');
    ph.className = 'mceNonEditable ai-stream-ph';
    ph.textContent = '';

    editor.undoManager.transact(() => {
      const rng = editor.selection.getRng();
      rng.deleteContents();
      rng.insertNode(ph);
      try {
        rng.setStartAfter(ph);
        rng.setEndAfter(ph);
        editor.selection.setRng(rng);
      } catch { /* empty */ }
    });

    try { editor.formatter.register('ai_pending', { inline: 'mark', styles: { backgroundColor: '#fff3cd' } }); } catch {}
    try { editor.formatter.apply('ai_pending', {}, ph); } catch { /* empty */ }

    const ctrl = new AbortController();
    const onEsc = (e:KeyboardEvent) => { if (e.key === 'Escape') ctrl.abort(); };
    document.addEventListener('keydown', onEsc);

    let buffer = '';
    const flush = (chunk:string) => {
      buffer += chunk;
      if (ph && ph.parentNode) {
        ph.textContent = buffer; // 后端纯文本（如返回 HTML 则改 ph.innerHTML = buffer）
      }
    };

    const safeReplaceFinal = (finalStr:string, isHtml:boolean) => {
      editor.undoManager.transact(() => {
        if (ph && ph.parentNode) {
          try {
            if (isHtml) {
              const tmp = doc.createElement('div');
              tmp.innerHTML = finalStr;
              const frag = doc.createDocumentFragment();
              while (tmp.firstChild) frag.appendChild(tmp.firstChild);
              ph.replaceWith(frag);
            } else {
              ph.replaceWith(doc.createTextNode(finalStr));
            }
          } catch {
            try {
              editor.dom.setOuterHTML(ph, isHtml ? finalStr : editor.dom.encode(finalStr));
            } catch {
              try { editor.selection.moveToBookmark(bookmark); } catch {}
              editor.insertContent(isHtml ? finalStr : editor.dom.encode(finalStr));
            }
          }
        } else {
          try { editor.selection.moveToBookmark(bookmark); } catch {}
          editor.insertContent(isHtml ? finalStr : editor.dom.encode(finalStr));
        }
      });
      editor.nodeChanged();
    };

    try {
      const reqData = { action, text: selText, modelName: props.modelName };

      await fetchEventSource(props.baseUrl + '' + other.adaptationUrl('/knowledge/ai-writer/process/text'), {
        method: 'POST',
        signal: ctrl.signal,
        headers: {
          Authorization: 'Bearer ' + Session.getToken(),
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(reqData),
        onopen: async (resp) => {
          if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
          const ct = resp.headers.get('content-type') || '';
          if (!ct.includes('text/event-stream')) {
            const data = await resp.text(); // 回退为一次性响应
            flush(data);
            ctrl.abort();
          }
        },
        onmessage: (ev) => {
          if (ev.data === '[DONE]') { ctrl.abort(); return; }
          try {
            const j = JSON.parse(ev.data);
            if (j?.choices?.[0]?.delta?.content) flush(j.choices[0].delta.content);
            else if (j?.delta) flush(j.delta);
            else if (j?.result) { flush(j.result); if (j.done) ctrl.abort(); }
          } catch {
            flush(ev.data);
          }
        },
        onerror: (err) => { throw err; },
        openWhenHidden: true,
      });

      try { ph && editor.formatter.remove('ai_pending', {}, ph); } catch {}
      safeReplaceFinal(buffer, /* isHtml */ false)  // 如后端是 HTML，改 true
      editor.notificationManager.open({ text: 'AI处理完成', type: 'success', timeout: 1200 });
    } catch (e:any) {
      try { ph && editor.formatter.remove('ai_pending', {}, ph); } catch {}
      safeReplaceFinal(selText, /* isHtml */ false)
      editor.notificationManager.open({ text: `AI调用失败：${e.message || e}`, type: 'error' });
    } finally {
      document.removeEventListener('keydown', onEsc);
    }
  };

  // 六个 AI 快捷按钮
  const regs:[string,string,()=>void][] = [
    ['aiPolish',   '润色',  () => runAI('polish')],
    ['aiRewrite',  '改写',  () => runAI('rewrite')],
    ['aiExtend',   '扩写',  () => runAI('extend')],
    ['aiImitate',  '仿写',  () => runAI('imitate')],
    ['aiContinue', '续写',  () => runAI('continue')],
    ['aiShorten',  '缩写',  () => runAI('shorten')],
  ];
  if (props.enableAI) {
    regs.forEach(([key, text, onAction]) => {
      editor.ui.registry.addButton(key, { text, icon: 'ai', tooltip: text, onAction });
    });
  }

}

const initOptions = ref({
  menubar: false,
  language: props.language,
  height: '100%',
  width: '100%',
  plugins: plugins.value,
  toolbar: props.enableAI ? toolbar : toolbar.map(g => (g.name==='other' ? ({...g, items: g.items.filter(i=>i!=='aiDocGen')}) : g)),
  body_class: 'doc-page',
  content_style: content_style.value,
  font_family_formats: font_family_formats.value,
  font_size_formats: font_size_formats.value,
  line_height_formats: line_height_formats.value,
  branding: false,
  skin_url: props.skinUrl,
  quickbars_selection_toolbar: props.enableAI ? 'aiPolish aiRewrite aiExtend aiImitate aiContinue aiShorten' : '',
  quickbars_insert_toolbar: '',
  contextmenu: 'cut copy paste | link | hr removeformat',
  setup: (editor:any) => setupFunction(editor),
  // 允许占位符节点保留 id / style，避免被过滤
  extended_valid_elements: 'span[id|class|style],div[id|class|style],mark[class|style]'
})

/* ======= 公文生成：供父组件调用（无 UI） ======= */
/** 立即返回 { abort, done }；不要 await 这个函数本身 */
function generateDoc(opts: {
  editor?: any,
  title: string,
  abstractContent?: string,
  modelName?: string,
  docTemplateId?: string,
  lockEditor?: boolean,      // 默认 true
  htmlStream?: boolean,      // 默认 true（后端返回 HTML 片段）
  onProgress?: (chunk: string, buffer: string) => void,
  onDone?: (finalHtml: string) => void,
  onError?: (err: Error) => void,
}): { abort: () => void; done: Promise<void> } {
  const {
    title,
    abstractContent = '',
    modelName = props.modelName,
    docTemplateId = '',
    lockEditor = true,
    htmlStream = true,
    onProgress,
    onDone,
    onError,
  } = opts || {}

  if (!title || !title.trim()) {
    // 与原来保持行为一致：直接抛错由调用方处理
    throw new Error('标题不能为空')
  }

  const abortController = new AbortController()
  const abort = () => abortController.abort()

  // 把异步流程装进 done 里
  const done = (async () => {
    try {
      // 1) 拿编辑器实例（可能需要等待）
      const ed = opts.editor ?? editorRef.value ?? await waitForReady()
      const editor = ed
      if (!editor) throw new Error('editor 实例缺失')

      const shell = document.getElementById('sample')
      if (lockEditor) {
        try { editor.mode.set('readonly') } catch {}
        try { shell?.classList.add('ai-busy') } catch {}
      }

      const doc = editor.getDoc()
      const phId = 'doc-stream-' + Date.now()
      const ph = doc.createElement('div')
      ph.id = phId
      ph.setAttribute('data-ai-doc-ph', phId)
      ph.setAttribute('contenteditable', 'false')
      ph.className = 'mceNonEditable ai-doc-stream-ph'
      ph.innerHTML = ''

      const bookmark = editor.selection.getBookmark(2, true)

      editor.undoManager.transact(() => {
        const rng = editor.selection.getRng()
        rng.deleteContents()
        rng.insertNode(ph)
        try {
          rng.setStartAfter(ph)
          rng.setEndAfter(ph)
          editor.selection.setRng(rng)
        } catch { /* empty */ }
      })

      const onEsc = (e: KeyboardEvent) => { if (e.key === 'Escape') abortController.abort() }
      document.addEventListener('keydown', onEsc)

      let buffer = ''
      let raf = 0
      const flush = (chunk: string) => {
        buffer += chunk
        if (!ph || !ph.parentNode) return
        if (!raf) {
          raf = requestAnimationFrame(() => {
            try {
              if (htmlStream) ph.innerHTML = buffer
              else ph.textContent = buffer
              editor.nodeChanged()
            } finally { raf = 0 }
            onProgress?.(chunk, buffer)
          })
        }
      }

      const safeReplaceFinal = (finalStr: string, isHtml: boolean) => {
        editor.undoManager.transact(() => {
          if (ph && ph.parentNode) {
            try {
              if (isHtml) {
                const tmp = doc.createElement('div')
                tmp.innerHTML = finalStr
                const frag = doc.createDocumentFragment()
                while (tmp.firstChild) frag.appendChild(tmp.firstChild)
                ph.replaceWith(frag)
              } else {
                ph.replaceWith(doc.createTextNode(finalStr))
              }
            } catch {
              try {
                editor.dom.setOuterHTML(ph, isHtml ? finalStr : editor.dom.encode(finalStr))
              } catch {
                try { editor.selection.moveToBookmark(bookmark) } catch {}
                editor.insertContent(isHtml ? finalStr : editor.dom.encode(finalStr))
              }
            }
          } else {
            try { editor.selection.moveToBookmark(bookmark) } catch {}
            editor.insertContent(isHtml ? finalStr : editor.dom.encode(finalStr))
          }
        })
        editor.nodeChanged()
      }

      const payload = { title, abstractContent, modelName, docTemplateId }

      // 2) 流式请求（用 signal 关联 abort）
      await fetchEventSource(props.baseUrl + '' + other.adaptationUrl('/knowledge/ai-writer/generate/document'), {
        method: 'POST',
        signal: abortController.signal,
        headers: {
          Authorization: 'Bearer ' + Session.getToken(),
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
        openWhenHidden: true,
        retry: false,
        onopen(res) {
          if (!res.ok) throw new Error(`请求失败：${res.status}`)
        },
        onmessage(ev) {
          if (ev.data === '[DONE]') { abortController.abort(); return }
          try {
            const j = JSON.parse(ev.data)
            const piece = j?.delta ?? j?.content ?? j?.text ?? j?.result ?? ''
            if (piece) flush(piece)
          } catch { flush(ev.data) }
        },
        onerror(err) { throw err },
      })

      // 3) 收尾
      safeReplaceFinal(buffer, /* isHtml */ htmlStream)
      onDone?.(buffer)

      // 清理
      document.removeEventListener('keydown', onEsc)
      if (lockEditor) {
        try { editor.mode.set('design') } catch {}
        try { shell?.classList.remove('ai-busy') } catch {}
      }
    } catch (err: any) {
      // AbortError 当成正常中止提示即可，避免误报错误
      if (err?.name === 'AbortError') {
        onError?.(err) // 如果不想抛给外层，也可以不回调
      } else {
        onError?.(err instanceof Error ? err : new Error(String(err)))
      }
      // 清理：这里重复一份兜底，避免编辑器锁死
      try {
        const ed = opts.editor ?? editorRef.value
        const shell = document.getElementById('sample')
        if (ed && lockEditor) {
          try { ed.mode.set('design') } catch {}
          try { shell?.classList.remove('ai-busy') } catch {}
        }
      } catch { /* empty */ }
      // 继续抛出让调用方 .catch 捕获（如果你不想抛出，可删除下一行）
      // throw err
    }
  })()

  // 立即把可用的 abort 句柄交出去
  return { abort, done }
}


// 暴露给父组件调用
defineExpose({
  waitForReady,
  getHtml,
  getEditor,
  setHtml,
  generateDoc,
})
</script>

<!-- 不加 scoped，保持你原有的全局样式命中 TinyMCE 内部类名与 #uuid_ifr -->
<style>
/* ✅ 工具栏充满整个页面 */
.tox-editor-header {
  width: 100vw !important;
  margin-left: calc(-50vw + 50%);
  box-sizing: border-box;
}
/* ✅ 编辑区宽度固定，居中 */
.tox-edit-area { max-width: 1000px; margin: 0 auto; }
/* 隐藏底部推广和状态栏 */
.tox-promotion, .tox-statusbar { display: none !important; }
.tox-sidebar-wrap { width: 80%; margin: 0 auto; }
.tox-tbtn--bespoke { width: 70px!important; background: #ffffff!important; }

#sample { width: 100%; height: 100%; }
#sample .tox-tinymce { border: none; }
#sample .tox-sidebar-wrap { width: 80%; margin: 0 auto; padding-top: 2%; }

.tox-edit-area:focus, .tox-edit-area__iframe:focus {
  outline: none !important; border: none !important; box-shadow: none !important; scrollbar-width: none!important;
}
.tox-edit-area:before { display: none; }
/* 这里依赖 #uuid_ifr，必须保留 editor 的 id="uuid" */
#uuid_ifr html { scrollbar-width: none; }

.tox-pop::before, .tox-pop::after { display: none !important; }

/* 生成时置灰，不盖遮罩 */
.ai-busy .tox .tox-toolbar,
.ai-busy .tox .tox-menubar,
.ai-busy .tox .tox-sidebar { pointer-events: none; opacity: .5; }

/* 占位符可见性（可选） */
.ai-stream-ph { background: rgba(255,243,205,.7); border-radius: 2px; padding: 0 2px; }
.ai-doc-stream-ph { background: rgba(226,232,240,.6); border-radius: 3px; }
</style>
